/**
 * Copyright 2019 Anthony Trinh
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ch.qos.logback.classic.net;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;

import javax.net.ServerSocketFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;

/**
 * A simple {@link SocketNode} based server.
 *
 * <pre>
 *      &lt;b&gt;Usage:&lt;/b&gt; java ch.qos.logback.classic.net.SimpleSocketServer port configFile
 * </pre>
 *
 * where <em>port</em> is a port number where the server listens and
 * <em>configFile</em> is an xml configuration file fed to
 * {@link JoranConfigurator}.
 *
 * @author Ceki G&uuml;lc&uuml;
 * @author S&eacute;bastien Pennec
 *
 * @since 0.8.4
 */
public class SimpleSocketServer extends Thread {

  Logger logger = LoggerFactory.getLogger(SimpleSocketServer.class);

  private final int port;
  private final LoggerContext lc;
  private boolean closed = false;
  private ServerSocket serverSocket;
  private List<SocketNode> socketNodeList = new ArrayList<SocketNode>();

  // used for testing purposes
  private CountDownLatch latch;

  public static void main(String argv[]) throws Exception {
    doMain(SimpleSocketServer.class, argv);
  }

  protected static void doMain(Class<? extends SimpleSocketServer> serverClass,
      String argv[]) throws Exception {
    int port = -1;
    if (argv.length == 2) {
      port = parsePortNumber(argv[0]);
    } else {
      usage("Wrong number of arguments.");
    }

    String configFile = argv[1];
    LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
    configureLC(lc, configFile);

    SimpleSocketServer sss = new SimpleSocketServer(lc, port);
    sss.start();
  }

  public SimpleSocketServer(LoggerContext lc, int port) {
    this.lc = lc;
    this.port = port;
  }


  public void run() {

    final String oldThreadName = Thread.currentThread().getName();

    try {

      final String newThreadName = getServerThreadName();
      Thread.currentThread().setName(newThreadName);

      logger.info("Listening on port " + port);
      serverSocket = getServerSocketFactory().createServerSocket(port);
      while (!closed) {
        logger.info("Waiting to accept a new client.");
        signalAlmostReadiness();
        Socket socket = serverSocket.accept();
        logger.info("Connected to client at " + socket.getInetAddress());
        logger.info("Starting new socket node.");
        SocketNode newSocketNode = new SocketNode(this, socket, lc);
        synchronized (socketNodeList) {
          socketNodeList.add(newSocketNode);
        }
        final String clientThreadName = getClientThreadName(socket);
        new Thread(newSocketNode, clientThreadName).start();
      }
    } catch (Exception e) {
      if(closed) {
        logger.info("Exception in run method for a closed server. This is normal.");
      } else {
        logger.error("Unexpected failure in run method", e);
      }
    }

    finally {
      Thread.currentThread().setName(oldThreadName);
    }
  }

  /**
   * Returns the name given to the server thread.
   */
  protected String getServerThreadName() {
    return String.format(Locale.US,"Logback %s (port %d)", getClass().getSimpleName(), port);
  }

  /**
   * Returns a name to identify each client thread.
   */
  protected String getClientThreadName(Socket socket) {
    return String.format(Locale.US, "Logback SocketNode (client: %s)", socket.getRemoteSocketAddress());
  }

  /**
   * Gets the platform default {@link ServerSocketFactory}.
   * <p>
   * Subclasses may override to provide a custom server socket factory.
   */
  protected ServerSocketFactory getServerSocketFactory() {
    return ServerSocketFactory.getDefault();
  }

  /**
   * Signal another thread that we have established a connection
   * This is useful for testing purposes.
   */
  void signalAlmostReadiness() {
    if(latch != null && latch.getCount() != 0) {
      //System.out.println("signalAlmostReadiness() with latch "+latch);
      latch.countDown();
    }
  }

  /**
   * Used for testing purposes
   * @param latch
   */
  void setLatch(CountDownLatch latch) {
    this.latch = latch;
  }
  /**
    * Used for testing purposes
    */
  public CountDownLatch getLatch() {
    return latch;
  }
  public boolean isClosed() {
    return closed;
  }

  public void close() {
    closed = true;
    if (serverSocket != null) {
      try {
        serverSocket.close();
      } catch (IOException e) {
        logger.error("Failed to close serverSocket", e);
      } finally {
        serverSocket = null;
      }
    }

    logger.info("closing this server");
    synchronized (socketNodeList) {
      for(SocketNode sn: socketNodeList) {
        sn.close();
      }
    }
    if(socketNodeList.size() != 0) {
      logger.warn("Was expecting a 0-sized socketNodeList after server shutdown");
    }

  }

  public void socketNodeClosing(SocketNode sn) {
    logger.debug("Removing {}", sn);

    // don't allow simultaneous access to the socketNodeList
    // (e.g. removal whole iterating on the list causes
    // java.util.ConcurrentModificationException
    synchronized (socketNodeList) {
      socketNodeList.remove(sn);
    }
  }

  static void usage(String msg) {
    System.err.println(msg);
    System.err.println("Usage: java " + SimpleSocketServer.class.getName()
        + " port configFile");
    System.exit(1);
  }

  static int parsePortNumber(String portStr) {
    try {
      return Integer.parseInt(portStr);
    } catch (java.lang.NumberFormatException e) {
      e.printStackTrace();
      usage("Could not interpret port number [" + portStr + "].");
      // we won't get here
      return -1;
    }
  }

  static public void configureLC(LoggerContext lc, String configFile)
      throws JoranException {
    JoranConfigurator configurator = new JoranConfigurator();
    lc.reset();
    configurator.setContext(lc);
    configurator.doConfigure(configFile);
  }
}
